﻿using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading;

namespace Borg
{
    public sealed class DispatcherOperation : IDispatcherOperation
    {
        private readonly object _monitor = new object();
        private DispatcherOperationStatus _status = DispatcherOperationStatus.Pending;

        public event UnhandledExceptionEventHandler OnUnhandledException;
        public event EventHandler Finished;

        private readonly Dispatcher _dispatcher;
        private readonly Delegate _delegate;
        private readonly object[] _params;

        private object _result;
        private Exception _unhandledException;
        private Handler _aborter;

        public DispatcherOperationStatus Status
        {
            get
            {
                lock (_monitor)
                    return _status;
            }
        }

        public object Result
        {
            get
            {
                Wait();
                return _result;
            }
        }

        public Exception UnhandledException
        {
            get
            {
                Wait();
                return _unhandledException;
            }
        }

        internal DispatcherOperation(Dispatcher dispatcher, Delegate @delegate, object[] @params)
        {
            if (@delegate == null)
                throw new ArgumentNullException("delegate");

            if (@params == null)
                throw new ArgumentNullException("params");
            
            _dispatcher = dispatcher;
            _delegate = @delegate;
            _params = @params;
        }

        public DispatcherOperationStatus Wait()
        {
            return Wait(0);
        }

        public DispatcherOperationStatus Wait(int millis)
        {
            switch (_status)
            {
                case DispatcherOperationStatus.Pending:
                case DispatcherOperationStatus.Executing:
                    if (_dispatcher.CheckAccess())
                        throw new DeadlockException();

                    var timeout = millis <= 0;

                    lock (_monitor)
                    {
                        while ((timeout || millis > 0) &&
                            (_status == DispatcherOperationStatus.Pending ||
                            _status == DispatcherOperationStatus.Executing))
                        {
                            if (timeout)
                                Monitor.Wait(_monitor);
                            else
                            {
                                var now = DateTime.Now;
                                Monitor.Wait(_monitor, millis);
                                millis -= (int) (DateTime.Now - now).TotalMilliseconds;
                            }
                        }

                        return _status;
                    }
                case DispatcherOperationStatus.Completed:
                case DispatcherOperationStatus.Aborted:
                    return _status;
                
                default:
                    Debug.Assert(false);
                    throw new InvalidOperationException();
            }
        }

        public void Abort()
        {
            lock (_monitor)
            {
                switch (_status)
                {
                    case DispatcherOperationStatus.Pending:
                        SetStatus(DispatcherOperationStatus.Aborted);
                        break;
                    
                    case DispatcherOperationStatus.Executing:
                        _aborter();
                        SetStatus(DispatcherOperationStatus.Aborted);
                        break;
                }
            }
        }

        public void Invoke(Handler aborter)
        {
            try
            {
                lock (_monitor)
                {
                    switch (_status)
                    {
                        case DispatcherOperationStatus.Pending:
                            _aborter = aborter;
                            SetStatus(DispatcherOperationStatus.Executing);
                            break;
                        
                        case DispatcherOperationStatus.Executing:
                        case DispatcherOperationStatus.Completed:
                            Debug.Assert(false);
                            throw new InvalidOperationException();
                        
                        case DispatcherOperationStatus.Aborted:
                            return;
                    }
                }

                _result = _dispatcher.Invoker.Invoke(_delegate, _params);
                SetStatus(DispatcherOperationStatus.Completed);
            }
            catch (TargetInvocationException ex)
            {
                _unhandledException = ex.InnerException;
                SetStatus(DispatcherOperationStatus.Aborted);
                FireOnUnhandledException();
            }
            catch (ThreadAbortException ex)
            {
                _unhandledException = ex;
                Thread.ResetAbort();
                SetStatus(DispatcherOperationStatus.Aborted);
            }
            catch (Exception ex)
            {
                _unhandledException = ex;
                SetStatus(DispatcherOperationStatus.Aborted);
            }
            finally
            {
                _aborter = null;
                FireFinishedEvent();
            }
        }

        private void SetStatus(DispatcherOperationStatus newStatus)
        {
            lock (_monitor)
            {
                _status = newStatus;
                Monitor.PulseAll(_monitor);
            }
        }

        private void FireOnUnhandledException()
        {
            if (OnUnhandledException == null)
                return;
            
            OnUnhandledException(this, _unhandledException);
        }

        private void FireFinishedEvent()
        {
            if (Finished == null)
                return;
            
            Finished(this, EventArgs.Empty);
        }

        public override string ToString()
        {
            return _delegate.ToString();
        }

        public delegate void UnhandledExceptionEventHandler(
            IDispatcherOperation sender, Exception exception);
    }
}
